package org.lemon.transport.context.service.queue;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.lemon.transport.context.data.queue.message.QueueMessage;
import org.lemon.transport.context.service.queue.stats.MessagesStats;
import org.lemon.transport.context.utils.DefaultThreadFactory;
import org.springframework.util.StopWatch;

import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class DefaultQueueRequestTemplate<Request extends QueueMessage, Response extends QueueMessage> implements QueueRequestTemplate<Request, Response> {

    private final QueueAdmin queueAdmin;
    private final QueueProducer<Request> requestTemplate;
    private final QueueConsumer<Response> responseTemplate;
    final ConcurrentHashMap<UUID, DefaultQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests = new ConcurrentHashMap<>();

    final ExecutorService executor;

    final long maxRequestTimeoutNs;
    final long maxRequestTimeout;
    final long maxPendingRequests;
    /**
     * 消费出队列的间隔时间
     */
    final long pollInterval;
    private volatile boolean stopped = false;
    private long nextCleanupNs = 0L;
    private final Lock cleanerLock = new ReentrantLock();

    /**
     * 消息统计
     */
    private MessagesStats messagesStats;

    @Builder
    public DefaultQueueRequestTemplate(QueueAdmin queueAdmin,
                                       QueueProducer<Request> requestTemplate,
                                       QueueConsumer<Response> responseTemplate,
                                       long maxRequestTimeout,
                                       long maxPendingRequests,
                                       long pollInterval,
                                       @Nullable ExecutorService executor) {

        this.queueAdmin = queueAdmin;
        this.pollInterval = pollInterval;
        this.requestTemplate = requestTemplate;
        this.responseTemplate = responseTemplate;
        this.maxRequestTimeoutNs = maxRequestTimeout;
        this.maxRequestTimeout = maxRequestTimeout;
        this.maxPendingRequests = maxPendingRequests;
        this.executor = executor == null ? createExecutor() : executor;
    }

    private ExecutorService createExecutor() {
        return Executors.newSingleThreadExecutor(DefaultThreadFactory.forName("queue-request-template-" + responseTemplate.getTopic()));
    }

    @Override
    public void init() {
        queueAdmin.createTopicIfNotExists(responseTemplate.getTopic());
        requestTemplate.init();
        responseTemplate.subscribe();
        executor.submit(this::mainLoop);
    }

    @Override
    public void setMessagesStats(MessagesStats messagesStats) {
        this.messagesStats = messagesStats;
    }

    void mainLoop() {
        while (!stopped) {
            StopWatch sw = new StopWatch();
            sw.start();
            try {
                fetchAndProcessResponses();
            } catch (Throwable e) {
                long sleepNanos = TimeUnit.MILLISECONDS.toNanos(this.pollInterval) - sw.getTotalTimeNanos();
                log.warn("Failed to obtain and process responses from queue. Going to sleep " + sleepNanos + "ns", e);
                sleep(sleepNanos);
            }
        }
    }

    void sleep(long nanos) {
        LockSupport.parkNanos(nanos);
    }

    void fetchAndProcessResponses() {
        final long pendingRequestsCount = pendingRequests.mappingCount();
        log.trace("Starting template pool topic {}, for pendingRequests {}", responseTemplate.getTopic(), pendingRequestsCount);
        List<Response> responses = doPoll();
        log.trace("Completed template poll topic {}, for pendingRequests [{}], received [{}] responses", responseTemplate.getTopic(), pendingRequestsCount, responses.size());
        responses.forEach(this::processResponse);
        responseTemplate.commit();
        tryCleanStaleRequests();

    }

    @Override
    public ListenableFuture<Response> send(Request request) {

        return send(request, this.maxRequestTimeoutNs);
    }

    @Override
    public ListenableFuture<Response> send(Request request, long timeoutNs) {

        if (pendingRequests.mappingCount() >= maxPendingRequests) {
            log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance. Request is {}", maxPendingRequests, request);
            return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!"));
        }
        UUID requestId = UUID.randomUUID();
        request.getHeaders().putRequestId(requestId);
        request.getHeaders().putResponseTopic(responseTemplate.getTopic());
        request.getHeaders().putExpireTs(getCurrentTimeMs() + maxRequestTimeout);
        long currentClockNs = getCurrentClockNs();
        SettableFuture<Response> future = SettableFuture.create();
        ResponseMetaData<Response> responseMetaData =
                new ResponseMetaData<>(currentClockNs, timeoutNs, currentClockNs + timeoutNs, future);
        log.trace("pending {}", responseMetaData);
        if (pendingRequests.putIfAbsent(requestId, responseMetaData) != null) {
            log.warn("Pending request already exists [{}]", requestId);
            return Futures.immediateFailedFuture(new RuntimeException("Pending request already exists !" + requestId));
        }
        sendToRequestTemplate(request, requestId, future, responseMetaData);
        return future;
    }

    @Override
    public void stop() {

        stopped = true;
        if (responseTemplate != null){
            responseTemplate.unsubscribe();
        }
        if (requestTemplate != null){
            requestTemplate.stop();
        }
        executor.shutdownNow();
    }

    public List<Response> doPoll() {
        return responseTemplate.poll(pollInterval);
    }

    void processResponse(Response response) {

        UUID requestId = response.getHeaders().gutRequestId();
        if (requestId == null) {
            log.error("[{}] Missing requestId in header and body", response);
        } else {
            log.trace("[{}] Response received: {}", requestId, response);
            ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);
            if (expectedResponse == null) {
                log.debug("[{}] Invalid or stale request, response: {}", requestId, String.valueOf(response).replace("\n", " "));
            } else {
                expectedResponse.future.set(response);
            }
        }
    }

    long getCurrentTimeMs() {
        return System.currentTimeMillis();
    }

    long getCurrentClockNs() {
        return System.nanoTime();
    }

    void sendToRequestTemplate(Request request, UUID requestId, SettableFuture<Response> future, ResponseMetaData<Response> responseMetaData) {

        log.trace("[{}] Sending request, key [{}], expTime [{}], request {}", requestId, request.getKey(), responseMetaData.expTime, request);
        if (messagesStats != null) {
            messagesStats.incrementTotal();
        }
        TopicPartitionInfo topic = new TopicPartitionInfo(requestTemplate.getDefaultTopic(), null, null, false);
        requestTemplate.send(topic, request, new QueueCallback() {
            @Override
            public void onSuccess() {
                if (messagesStats != null) {
                    messagesStats.incrementSuccessful();
                }
                log.trace("[{}] Request sent: {}, request {}", requestId, responseMetaData, request);
            }

            @Override
            public void onFailure(Throwable t) {
                if (messagesStats != null) {
                    messagesStats.incrementFailed();
                }
                pendingRequests.remove(requestId);
                future.setException(t);
            }
        });
    }

    private boolean tryCleanStaleRequests() {
        if (!cleanerLock.tryLock()) {
            return false;
        }
        try {
            log.trace("tryCleanStaleRequest...");
            final long currentNs = getCurrentClockNs();
            if (nextCleanupNs < currentNs) {
                pendingRequests.forEach((key, value) -> {
                    if (value.expTime < currentNs) {
                        ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
                        if (staleRequest != null) {
                            setTimeoutException(key, staleRequest, currentNs);
                        }
                    }
                });
            }
        } finally {
            cleanerLock.unlock();
        }
        return true;
    }

    void setTimeoutException(UUID key, ResponseMetaData<Response> staleRequest, long currentNs) {
        if (currentNs >= staleRequest.getSubmitTime() + staleRequest.getTimeout()) {
            log.debug("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
        } else {
            log.info("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
        }
        staleRequest.future.setException(new TimeoutException());
    }

    @Getter
    static class ResponseMetaData<T> {

        private final long submitTime;
        private final long timeout;
        private final long expTime;
        private final SettableFuture<T> future;

        public ResponseMetaData(long submitTime, long timeout, long expTime, SettableFuture<T> future) {
            this.submitTime = submitTime;
            this.timeout = timeout;
            this.expTime = expTime;
            this.future = future;
        }

        @Override
        public String toString() {
            return "ResponseMetaData{" +
                    "submitTime=" + submitTime +
                    ", calculatedExpTime=" + (submitTime + timeout) +
                    ", deltaMs=" + (expTime - submitTime) +
                    ", expTime=" + expTime +
                    ", future=" + future +
                    '}';
        }
    }
}
